-
Notifications
You must be signed in to change notification settings - Fork 29.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Sort Order Option setting to address #27759 #96214
Conversation
|
||
// A collator with numeric sorting enabled. | ||
const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { | ||
const collator = new Intl.Collator(undefined, { numeric: true }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the sensitivity: base
option here. It was doing more harm than good since the code was falling back to a unicode comparison when there were accent or case differences.
} | ||
|
||
return result; | ||
// Using the numeric option in the collator will make compare(`foo1`, `foo01`) === 0. Sort the shorter name first. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Old code sorted in unicode order here - and not just if the lengths were different - if there was any kind of difference between the strings including case or accent.
return oneName.length < otherName.length ? -1 : 1; | ||
} | ||
|
||
// Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extensions are a case where it seems best to use a case-insensitive (but not accent-insensitive) check before disambiguating numbers. Their case only comes into play at the very end if everything else has no difference. This makes sense because .MD
and .md
are treated as the same thing when determining file type.
return [(match && match[1]) || '', (match && match[3]) || '']; | ||
let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || '']; | ||
|
||
// treat an empty filename with an extension, or a filename that starts with a dot, as a dotfile name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this special handling of dotfiles as mentioned in the PR submission comment.
* 0 otherwise | ||
* ``` | ||
*/ | ||
export function compareCaseLowerFirst(one: string, other: string): number { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I exported these functions so I could test them. They seem simple, but this is actually one of the main areas that I tweaked. I originally wasn't sure whether or not uppercase and lowercase should be considered greater than non-case letters or not, and whether a word that starts with a non-case character like an underscore should be non-case because of that underscore, or should be judged by the first character that has a case.
I can remove the exports and the tests now if desired.
// Using the numeric option in the collator will | ||
// make compare(`foo1`, `foo01`) === 0. We must disambiguate. | ||
if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && a !== b) { | ||
return a < b ? -1 : 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the unicode comparison here.
export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number { | ||
const a = one || ''; | ||
const b = other || ''; | ||
const result = intlFileNameCollator.getValue().collator.compare(a, b); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that unlike the other comparisons, this one wasn't separately comparing names and extensions - which cause problems like the aggregate_repo.go
vs aggregate.go
issue.
} | ||
|
||
const FileNameMatch = /^(.*?)(\.([^.]*))?$/; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't change this. I just moved it down next to the function that uses it.
|
||
if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && oneName !== otherName) { | ||
return oneName < otherName ? -1 : 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the unicode comparison here.
} | ||
|
||
function extractNameAndExtension(str?: string | null): [string, string] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... this one is exported in my code too? I'll remove the export.
return one < other ? -1 : 1; | ||
} | ||
|
||
/** Compares filenames by name unicode value, then extension unicode value. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function doesn't seem to be used anywhere except in extHostDocumentData.test.ts. Could it possibly be removed? Or does it need to stick around? Same with compareByPrefix...
}); | ||
|
||
test('compareCaseLowerFirst', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can remove these tests and not export these functions if that's preferred. The compareCase* functions are helper functions. It was good to be able to test them directly during development, but the other tests cover their behavior indirectly.
Thanks for the PR. Originaly I thought that this PR would only touch changes on the Explorer, however I see there are a lot of changes in the Comparer.ts thus also assigning @bpasero and @jrieken to take a look. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes in comparers.ts
are beyond of what I can review just now. I am a bit worried that this change impacts more than just the explorer (e.g. we use comparers in quick open). I would opt for a solution that keeps the comparers as they work today for anything that is not the explorer where we want to provide the new search options.
@@ -398,6 +398,19 @@ configurationRegistry.registerConfiguration({ | |||
], | |||
'description': nls.localize('sortOrder', "Controls sorting order of files and folders in the explorer.") | |||
}, | |||
'explorer.sortOrderOption': { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is it possibly to understand the difference between explorer.sortOrder
setting and explorer.sortOrderOption
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/vs/base/common/comparers.ts
Outdated
// Export compareFileNamesNumeric by its old name too for backwards compatibility. | ||
// Also keeping a placeholder parameter for the same reason. The parameter was sometimes supplied but was | ||
// not used in the original code. | ||
export { compareFileNamesNumeric as compareFileNames }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we seem to be supporting the old name, why are there changes in this PR to change to compareFileNamesNumeric
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really want to support the old name - but I wasn't sure how to safely change it? If I don't support the old name there are problems in CI building the stand-alone editor.
I've updated the code so that instead of just exporting an alias (which didn't work with tree-shaking) I export a compareFileNames function that calls compareFileNamesNumeric - so now the CI checks pass at least.
- check for active view descriptors - add model tests
Fix code comment block for Pseudoterminal (missing closing backticks)
Thanks for the responses @isidorn and @bpasero. Totally understand waiting until May for the full review. It's a bigger change then I was expecting it to be too! @bpasero I'm totally open to making the old functions - Once you do have a chance to review things you can let me know if you think any of the changes I made for |
P.S. I merged the latest changes from master into my fork and into my PR branch - which may not have been the best idea during the review. Just let me know if you'd like me to do something about that - and if so what's the best way to handle it. I did read through your coding guidelines but I'm not too familiar with your preferred workflow. Thanks! :-) |
@leilapearson thanks again for this PR. I see you put a lot of work and effort in this PR and we really appreciate it. However there are a couple of issues for us to proceed further with this:
Thank you again and I appologise if I did not specify this before you started on the implementation. |
Thanks for getting back to me @isidorn.
Since both options include submitting a PR with just the numeric sort algorithm fixes, I'll go ahead with that part while awaiting your response. Thanks again @isidorn for the continued support and guidance. I did put a lot of effort into this but I don't at all mind putting in some more to make this easier to review. |
@leilapearson let's go with 3a. Thanks! |
Will do @isidorn. Thanks! |
Closing this PR. Per our discussion, I'm splitting this PR into two separate PRs for easier review:
I've already created PR #97200 for the first part. I will create the second PR soon. |
This PR fixes #27759 by adding a Sort Order Option setting which can be found just below the existing Sort Order setting under Features -> Explorer .
To make it easier to see the effects of the change and the new settings, I created this repo:
The results of applying the old code as well as the new code with all combinations of Sort Order Option settings can be found here:
For the most part, sorts using the Sort Order Option default value of
numeric
will behave like sorts did before this option existed - but there are a few intentional changes. These are noted in the spreadsheet above, but here is the summary:The old numeric sort did not compare names separately from extensions. This caused
aggregate_repo.go
to sort beforeaggregate.go
because when using a locale-based sort the underscore character sorts before all alphabetic characters. By comparing names first and then extensions, the longer name will sort after the shorter name.The old numeric sort used
base
sensitivity - but then had a check to see if the strings were unequal and resolved differences using a unicode sort order. This meant that case differences and accent differences were being taken into account, but according to unicode order, not locale order, which should not have been the case. The new code leaves the sensitivity setting alone (which normally defaults to variant) so that things will sort properly according to platform locale - which means it checks first if strings differ in base characters, then if they differ in character accents, and finally if they differ in case, and sorts based on what users would expect.The old code treated any dotfile that had a leading dot and no other dots in the name as if it was an empty name plus an extension. It treated a dotfile with a leading dot and one or more other dots as if it was a name that started with a dot plus an extension. The new code treats the whole dotfile name as a name that starts with a dot and doesn't have an extension at all. This old behavior caused dotfiles that didn't have a second dot to appear between groups of files sorted by type - since the dotfiles themselves were treated as types. This seemed odd and inconsistent. For example,
.eslintrc.json
would sort with other.json
files in thej
section, which seemed like potentially a good thing - but.eslintignore
would sort in between.cs
files and.fs
files - far from.eslintrc.json
in thej
extensions section. People are more used to seeing all dotfiles together, regardless of extension, so that's what the new code does.There were quite a few decisions to make in how the various sort algorithms should behave - so I wound up tweaking things a number of times before I settled on the above behavior. I tried to follow the principle of least surprise when choosing between ways of handling things. Hopefully I succeeded, but if not, further tweaks can certainly be made.
CC: @isidorn